Skip to content

refactor: エージェント抽象を導入して Codex / Grok の差し込み口を作る(Phase A) - #97

Merged
takecchi merged 4 commits into
mainfrom
codiva/codiva-claude-codex-grok
Aug 8, 2026
Merged

refactor: エージェント抽象を導入して Codex / Grok の差し込み口を作る(Phase A)#97
takecchi merged 4 commits into
mainfrom
codiva/codiva-claude-codex-grok

Conversation

@takecchi

@takecchi takecchi commented Aug 8, 2026

Copy link
Copy Markdown
Owner

何のための PR か

codiva は Claude Code 専用でしたが、将来 Codex / Grok に対応できる形にするための土台を入れます。
挙動は変えず、差し込み口だけを作る Phase A です(アダプタ本体の実装は Phase B 以降)。

さらに「Claude で実装を進めていたが途中から Codex に切り替える」というユースケースを想定した
状態モデルまで入れてあります。

構造の変更

SDKMessage を直接 SessionState へ畳み込んでいた sdk-parse.ts を 2 段に割りました。

provider のメッセージ ─[アダプタの parse]→ AgentEvent[] ─[applyAgentEvent]→ SessionState
   SDKMessage             claude-parse.ts      agent-events.ts
  (Codex/Grok の形)      (各アダプタ)        (全 provider 共通)
新しいファイル 役割
core/agent-ports.ts AgentAdapter / AgentRun / AgentCapabilities / PermissionDecision = DI 境界(leaf)
core/agent-events.ts AgentEvent の語彙 + 全 provider 共通の畳み込み applyAgentEvent
core/claude-parse.ts sdk-parse.tsSDKMessageAgentEvent[] の写像に専念
core/claude-errors.ts Claude CLI の文言 / typed kind / HTTP status → AgentStopCause の分類
core/claude-adapter.ts query() の組み立てと canUseToolrequestPermission の写像

境界を SessionHandle / AgentAdapter に引いた理由(QueryFn ではない)

  • SessionManager から上(UI・永続化・PR 自動化・worktree・通知)は既に SessionHandle 越しにしか
    セッションを触っておらず、もともとエージェント非依存でした。その下にアダプタを足すだけで済みます。
  • 逆に SDK の query() の署名(AsyncIterable<SDKUserMessage> + Options + canUseTool +
    control request)を共通 IF にすると、全 provider に Claude の制御モデルの模倣を強いることになります。

副次的に直ったこと

  • 中立モジュールから SDK の import が消えた@anthropic-ai/claude-agent-sdk を import するのは
    claude-*.ts(アダプタ 3 点)だけになりました。
  • PermissionResult(SDK 型)が core に漏れていたのを自前の PermissionDecision に置換
    PermissionRequest は元から自前だったので、ようやく対になりました)。
  • Session.consumerate_limit_event を直接読んでいた既存の規約違反(「形の知識は 1 か所」)を解消。
  • 失敗の分類(認証切れ / レート制限 / 通信断)が reducer の正規表現から AgentAdapter.classifyError へ。
    aborted イベントは cause を運びます。文言の見分け方は provider ごとの知識なので状態機械には置きません。

セッション途中のエージェント切替

worktree(=実際の成果物)は provider 非依存なので引き継げますが、モデル側の文脈は provider を
またげません
(各 CLI が自分のトランスクリプトを持つため)。したがって切替は
「今のターンを畳む → 同じ worktree で別 provider の新セッションを開く」形になります。

引き継がれる 引き継がれない
worktree(コード・コミット・ブランチ) モデル側の会話文脈
codiva 側のログ・TODO・コスト・PR 解決済みモデル名
  • SessionState.agentSessionsprovider ごとの resume id を持たせ、永続化しました。
    Claude → Codex → Claude と戻ると Claude 側は続きから再開します(新規会話になりません)。
  • LogEntry.agent でどこからが別エージェントの発言かを刻みます。切替を使っていないセッションには
    付かない
    ので、既存の表示・ログの形は一切変わりません。
  • Session.setAgent() / getAgent()CodivaEventagent_switched を追加。
  • 旧スナップショットは 'claude' にフォールバックするので、既存の state.json はそのまま読めます。

/agent コマンドと引き継ぎプロンプトの生成は Phase B です。

挙動が変わらないことの担保

  • 1,100 行超の実データテストclaude-parse.spec.ts + __fixtures__/*.jsonl)は、分割前と
    同じ入口(旧 applySdkMessageapplyClaudeMessage)を叩き続けます = 分割のリグレッション網。
  • 変更が必要だったテストは status-reducer.spec.ts の 4 件だけで、これは分類が
    アダプタへ移ったことの反映です(文言 → 分類のテストは claude-errors.spec.ts
    classifyClaudeError に移設・拡充)。
  • agent-events.spec.ts を新規追加(19 件)。切替時の resume id の退避・復元、完了ゲート、
    rollup ガード、PR 検出、通知の畳み込みをカバー。

テスト

  • npm run lint … 211 ファイル通過
  • npm run typecheck … 通過
  • npm test2,277 件通過 / カバレッジ statements 95.07%・branches 90.42%(閾値 80 / 75)
  • npm run build … 通過(dist/index.js + dist/main-<hash>.js の 2 ファイル構成も維持)
  • 手動確認(TTY + 認証が必要なためエージェントセッションでは実行不可。挙動不変の
    リファクタリングであり配線は統合テストで検証済みですが、体感確認はお願いします)

ついでに直したもの

tests/update.test.tsx の 2 件が既存の flake でした(unavailable を検証する 2 つだけ
await flush() が抜けており、ダイアログが「確認中…」のまま assert していた)。兄弟テストと
同じ待ち方に揃えています。

ドキュメント

docs/ARCHITECTURE.md に「エージェント抽象」節を新設し、上記の設計判断を理由つきで記録しました。
CLAUDE.md の不変条件 2・3・4、.claude/rules/{sdk-integration,session-domain,architecture}.md
docs/PRD.md / docs/TASKS.md(Phase A ✅ / B〜D 未着手)も更新済みです。
README.md はユーザー可視の挙動が変わらないため無変更です。

次の Phase(この PR には含みません)

Phase 内容
B ACP(Agent Client Protocol)アダプタ + Codex 対応。session/request_permissioncanUseTool に、session/cancelinterrupt に、session/loadresume にほぼ 1:1 で対応します
C Grok 対応(grok agent stdio
D capability による UI 縮退の配線・/agent コマンド・引き継ぎプロンプトの生成

`unavailable` を検証する 2 件だけ `runUpdateCommand` の直後に assert していて、
他の同種テストにある `await flush()` が抜けていた。ダイアログは「確認中…」を
一度描いてから結果に差し替わるため、負荷の高い並列実行では確認中のまま
assert して落ちる。兄弟テストと同じ待ち方に揃える。
Codex / Grok など Claude 以外のコーディングエージェントに対応できる形にする
Phase A。挙動は変えず、差し込み口だけを作る。

## 何をしたか

SDK メッセージを直接 SessionState へ畳んでいた `sdk-parse.ts` を 2 段に割った:

    provider のメッセージ ─[アダプタの parse]→ AgentEvent[] ─[applyAgentEvent]→ SessionState

- `core/agent-events.ts` … provider 非依存のイベント語彙と**共通の畳み込み**。
  ログの上限・進捗・サブエージェントの完了ゲート・PR 検出・コスト集計はここ。
- `core/agent-ports.ts` … `AgentAdapter` / `AgentCapabilities` / `AgentRun` /
  `PermissionDecision`。新しいエージェントはこれを実装するだけで済む。
- `core/claude-parse.ts` / `claude-errors.ts` / `claude-adapter.ts` …
  Claude 固有の知識(メッセージの形・CLI の文言・`query()` の署名)の置き場所。

境界は `SessionHandle` / `AgentAdapter` に引いた。`QueryFn` ではない —
あれは Claude の control-request モデルそのもので、共通 IF にすると全 provider が
それを模倣する羽目になる。

## 副次的に直ったこと

- **core の中立モジュールから SDK の import が消えた**。SDK を import するのは
  `claude-*.ts`(アダプタ)だけになり、`status-reducer.ts` / `config.ts` は
  自前の型・定数で閉じた。
- `PermissionResult`(SDK 型)が core に漏れていたのを自前の `PermissionDecision`
  に置き換えた(`PermissionRequest` は元から自前だったので、ようやく対になった)。
- `Session.consume` が `rate_limit_event` を直接読んでいた規約違反
  (「形の知識は 1 か所」)を解消した。
- 失敗の分類(認証切れ / レート制限 / 通信断)が reducer から
  `AgentAdapter.classifyError` へ移り、`aborted` イベントは `cause` を運ぶように
  なった。文言の正規表現は provider ごとの知識なので状態機械には置かない。

## セッション途中のエージェント切替に備えた状態

worktree(成果物)は provider 非依存なので、Claude で進めた作業を途中から Codex に
引き継げる。ただしモデル側の文脈は provider をまたげない(各 CLI が自分の
トランスクリプトを持つ)ため、切替は「今のターンを畳む → 同じ worktree で別
provider の新セッションを開く」になる。

- `SessionState.agentSessions` に **provider ごとの resume id** を持たせ、
  永続化もした。Claude → Codex → Claude と戻ると続きから再開できる。
- `LogEntry.agent` でどこからが別エージェントの発言かを刻む。切替を使っていない
  セッションでは付かないので、既存の表示は一切変わらない。
- `Session.setAgent()` / `getAgent()` と `CodivaEvent` の `agent_switched` を追加。

`/agent` コマンドと引き継ぎプロンプトの生成は Phase B。

## 検証

lint / typecheck / test(2277 件)/ build すべて通過。1,100 行超の実データテストは
入口(旧 `applySdkMessage` → `applyClaudeMessage`)を変えずに残してあり、
分割のリグレッション網になっている。
Phase A(Codex / Grok の差し込み口づくり)の実装に合わせてドキュメントを更新する。

- `docs/ARCHITECTURE.md` … 「エージェント抽象」節を新設。境界を `SessionHandle` /
  `AgentAdapter` に引いた理由(`QueryFn` を共通 IF にすると全 provider に Claude の
  control-request モデルの模倣を強いる)、中立モジュールが SDK を import しない
  という線引き、畳み込みを共通化して写像だけをアダプタに置いた構造、そして
  セッション途中の切替で**何が引き継がれて何が引き継がれないか**を書いた。
- `CLAUDE.md` … 「コードの地図」に「別のエージェントに対応させる」の行を追加。
  不変条件 3 を「SDK の形を知るのは sdk-parse.ts だけ」から「エージェント固有の
  知識はアダプタに閉じる」へ書き直した。
- `.claude/rules/sdk-integration.md` … 2 段構成(アダプタの parse → 共通の fold)と、
  中立モジュールは SDK を import しないルールを明記。
- `.claude/rules/session-domain.md` … `agent_switched` / `aborted.cause` を
  `CodivaEvent` の表に追加し、永続化の節に `agent` / `agentSessions` を足した。
- `.claude/rules/architecture.md` … DI seam の節に `core/agent-ports.ts` を追加。
- `docs/PRD.md` / `docs/TASKS.md` … 「他エージェント対応 … 未実装」を Phase A 完了に
  更新し、Phase B(ACP アダプタ + Codex)/ C(Grok)/ D(capability による UI 縮退・
  `/agent`・引き継ぎプロンプト)を未着手として残した。

README.md は変更なし(ユーザー可視の挙動は変わっていない)。
`AgentCapabilities` は Phase A で「表」と `getAgent()` までが入っただけで、
それを見て UI を縮退させる配線は Phase D。読んだ人が「もう縮退する」と
誤解しないよう明記した。併せて不変条件 2(状態遷移は `reduce` と
`applyAgentEvent` の 2 本)と 4(i18n の例外にエージェント名を追加)を補足。
@takecchi
takecchi merged commit 9e20d51 into main Aug 8, 2026
1 check passed
@takecchi
takecchi deleted the codiva/codiva-claude-codex-grok branch August 8, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant